/*==========================================================================*\
 |  $Id: Exceptions.java 396 2011-04-10 20:37:18Z vtwoods@gmail.com $
 |*-------------------------------------------------------------------------*|
 |  Copyright (C) 2009 Virginia Tech
 |
 |  This file is part of CloudSpace.
 |
 |  CloudSpace is free software; you can redistribute it and/or modify
 |  it under the terms of the GNU General Public License as published
 |  by the Free Software Foundation; either version 3 of the License, or
 |  (at your option) any later version.
 |
 |  CloudSpace is distributed in the hope that it will be useful,
 |  but WITHOUT ANY WARRANTY; without even the implied warranty of
 |  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 |  GNU General Public License for more details.
 |
 |  You should have received a copy of the GNU General Public License
 |  along with CloudSpace; if not, see <http://www.gnu.org/licenses/>.
\*==========================================================================*/

package student.web.internal;

import java.io.StringWriter;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.zkoss.zhtml.Pre;
import org.zkoss.zhtml.Text;
import org.zkoss.zk.ui.Component;
import org.zkoss.zul.Grid;
import org.zkoss.zul.Row;
import org.zkoss.zul.Rows;

import student.web.Window;

//-------------------------------------------------------------------------
/**
 *  A set of static methods for use by ZK infrastructure classes to
 *  extract information from and about exceptions.  Since all methods are
 *  static, the constructor is private.
 *
 *  @author Stephen Edwards
 *  @author Last changed by $Author: vtwoods@gmail.com $
 *  @version $Revision: 396 $, $Date: 2009-11-05$
 */
public class Exceptions
{
    //~ Constructor ...........................................................

    // ----------------------------------------------------------
    /**
     * This constructor is private, since this class only exports static
     * methods and should not be instantiated.
     */
    private Exceptions()
    {
        // Nothing to do but hide this constructor
    }


    //~ Methods ...............................................................

    // ----------------------------------------------------------
    /**
     * Drills down using {@link Throwable#getCause()} to find the root
     * cause of this (possibly wrapped) exception.
     * @param t The exception to inspect
     * @return The bottom-most exception contained within t, or t itself if
     * there is no nested or chained exception.
     */
    public static Throwable getRealCause(Throwable t)
    {
        return org.zkoss.lang.Exceptions.getRealCause(t);
    }


    // ----------------------------------------------------------
    /**
     * Get this exception's class name, without any package prefix.
     * @param t The exception to inspect
     * @return The class name without its package part
     */
    public static String simplifiedExceptionName(Throwable t)
    {
        String name = getRealCause(t).getClass().getName();
        int pos = name.lastIndexOf('.');
        if (pos >= 0)
        {
            name = name.substring(pos + 1);
        }
        return name;
    }


    // ----------------------------------------------------------
    /**
     * Counts the number of times a given character occurs in a given
     * string.
     * @param c The character to look for
     * @param phrase The string to search in
     * @return A count of how many c characters are in the phrase.
     */
    private static int countOccurrences(char c, String phrase)
    {
        int result = 0;
        int pos = phrase.indexOf(c);
        while (pos >= 0)
        {
            ++result;
            pos = phrase.indexOf(c, pos + 1);
        }
        return result;
    }


    // ----------------------------------------------------------
    /**
     * Extract the message from an exception and generate an explanation
     * for it, if possible.  The explanation may include HTML markup.
     * @param exception The exception to explain
     * @return An explanation in HTML markup, if one can be generated, or
     * null if no explanation can be added.
     */
    public static String explain(Throwable exception)
    {
        String explanation = null;
        String message = exception.getMessage();
        if (exception instanceof org.xml.sax.SAXParseException
            || exception instanceof org.zkoss.util.IllegalSyntaxException)
        {
            explanation = "This error indicates there is incorrect or "
                + "malformed textual markup in the page that is being "
                + "viewed.";

            System.out.println("message:\n" + message);
            if (message.matches("(?s).*must be followed by either attribute"
                + "\\s+specifications, \">\" or \"/>\".*"))
            {
                explanation += "  Be sure that you have not accidentally "
                    + "included any double quotes in your attribute values.  "
                    + "A double quote (\") appearing inside an attribute "
                    + "value must be written as &amp;quot;.";
            }
        }
        else if (exception instanceof
            org.zkoss.zk.ui.metainfo.PropertyNotFoundException
            && message != null)
        {
            Matcher m = BAD_TAG_ATTRIBUTE.matcher(message);
            if (m.matches())
            {
                String varName = m.group(1);
                String tagName = m.group(2);
                if (varName.length() > 0)
                {
                    varName = varName.substring(0, 1).toLowerCase()
                        + varName.substring(1);
                }
                int pos = tagName.lastIndexOf('.');
                if (pos > 0)
                {
                    tagName = tagName.substring(pos + 1);
                }
                if (tagName.length() > 0)
                {
                    tagName = tagName.substring(0, 1).toLowerCase()
                        + tagName.substring(1);
                }
                if (varName.equals("onClick") || varName.equals("onChange"))
                {
                    explanation = "This error indicates that you have "
                        + "incorrectly added an \"<code>"
                        + varName
                        + "</code>\" attribute on some tag when you should "
                        + "have used a nested <code>&lt;attribute name=\""
                        + varName
                        + "\"&gt;</code> child element instead, somewhere in "
                        + "the page that is being viewed.";
                }
                else
                {
                    message = "Unrecognized attribute " + varName
                        + "on tag <" + tagName + ">";
                    explanation = "This error may indicate that you have "
                        + "incorrectly used an attribute called \"<code>"
                        + varName
                        + "</code>\" on a &lt;"
                        + tagName
                        + "&gt; tag that does not support such "
                        + "an attribute, somewhere in the "
                        + "page that is being viewed.";
                }
            }
        }
        else if (exception instanceof bsh.EvalError)
        {
            // First, look for a broken @{...} expression serving as an rvalue

            // Message should look like this if @{...} uses an undefined
            // method:
            // Sourced file: inline evaluation of: 1: _z_9rq_d_value.bean =
            // foo.methodDoesNotExist(); : Error in method invocation:
            // Method methodDoesNotExist() not found in class'Foo'
//            System.out.println("Raw bsh message: " + exception.getMessage());
            Matcher m = BAD_AT_EXPRESSION.matcher(exception.getMessage());
            if (m.find())
            {
                String expression = m.group(1);
                explanation = "This error was produced while evaluating "
                    + "<code>@{"
                    + expression
                    + "}</code>, which is used to define the value of an "
                    + "attribute on a tag somewhere in the page that is being "
                    + "viewed.";

                String remainder =
                    exception.getMessage().substring(m.end()).trim();
                m = METHOD_INVOCATION_MSG.matcher(remainder);
                if (m.matches())
                {
                    explanation = explanation
                        + "  It means that you tried to call the method "
                        + "<span style=\"color:red;font-weight:bold\"><code>"
                        + m.group(1)
                        + "</code></span> on an object of class <b><code>"
                        + m.group(2)
                        + "</code></b>, but that class does not define that "
                        + "method.";
                }
                else
                {
                    if (TOKEN_PARSING_EOF_MSG.matcher(remainder).matches()
                        || PARSE_ERROR_MSG.matcher(remainder).matches())
                    {
                        if (countOccurrences('"', expression) % 2 != 0)
                        {
                            explanation += "  Here, it appears that your "
                                + "attribute expression may be missing (or "
                                + "may have an extra) double quote.  Remember "
                                + "that a double quote (\") appearing inside "
                                + "an attribute value must be written as "
                                + "&amp;quot;.";
                        }
                        else if (countOccurrences('\'', expression) % 2 != 0)
                        {
                            explanation += "  Here, it appears that your "
                                + "attribute expression may be missing (or "
                                + "may have an extra) single quote.";
                        }
                        else if (countOccurrences('(', expression) !=
                            countOccurrences(')', expression))
                        {
                            explanation += "  Here, it appears that your "
                                + "attribute expression does not have "
                                + "balanced parentheses.";
                        }
                        else if (countOccurrences('[', expression) !=
                            countOccurrences(']', expression))
                        {
                            explanation += "  Here, it appears that your "
                                + "attribute expression does not have "
                                + "balanced square brackets.";
                        }
                        else if (countOccurrences('{', expression) !=
                            countOccurrences('}', expression))
                        {
                            explanation += "  Here, it appears that your "
                                + "attribute expression does not have "
                                + "balanced curly braces.";
                        }
                        else
                        {
                            explanation += "  It means that your attribute "
                                + "expression has a punctuation error, such "
                                + "as an unmatched delimiter.";
                        }
                    }
                    else
                    {
                        m = CANNOT_ACCESS_FIELD_MSG.matcher(remainder);
                        if (m.find())
                        {
                            String method = m.group(1);
                            if (method.length() > 0)
                            {
                                method = "get"
                                    + Character.toUpperCase(method.charAt(0))
                                    + method.substring(1)
                                    + "()";
                            }
                            explanation += "  It means that <b>there is no "
                                + "method named <code>"
                                + method
                                + "</code></b> that can be used to evaluate "
                                + "the attribute expression <code>@{"
                                + expression
                                + "}</code>.  Check that you have typed your "
                                + "expression correctly, and that your class "
                                + "defines the appropriate getter method.";
                        }
                    }
                }
            }
            else
            {
                if (VARIABLE_VALUE.matcher(message).find())
                {
                    explanation = "This error was produced while evaluating "
                        + "the value for a &lt;variable&gt; tag on your page.";
                }
                m = METHOD_INVOCATION_MSG.matcher(message);
                if (m.find())
                {
                    explanation = explanation
                        + "  It means that you tried to call the method "
                        + "<span style=\"color:red;font-weight:bold\"><code>"
                        + m.group(1)
                        + "</code></span> on an object of class <b><code>"
                        + m.group(2)
                        + "</code></b>, but that class does not define that "
                        + "method.";
                }
            }

            if (explanation == null)
            {
                explanation = "This error was produced while evaluating Java "
                    + "code that was embedded somewhere in your page.";
            }
        }else
        {
            StackTraceElement[] elements = exception.getStackTrace();
            if(elements != null && elements.length>0 && elements[0].getFileName().equals( "Reflect.java" ))
            {
                explanation = "This error was produced while evaluating java code embeded in your ZHTML page.";
            }
        }
        return explanation;
    }


    // ----------------------------------------------------------
    /**
     * Extract the message from an exception and add HTML
     * highlighting and formatting to it.  The highlighting is driven
     * by the typical structure of bean shell exceptions, and other
     * types of exceptions will simply be HTML-escaped.
     * @param exception The exception to highlight
     * @return The exception's message, with appropriate highlighting
     */
    public static String highlightExceptionMessage(Throwable exception)
    {
        String message = escapeHtml(exception.getMessage());
//        System.out.println("highlighting:" + message);
        if (exception instanceof bsh.EvalError)
        {
            Matcher m = BAD_AT_EXPRESSION.matcher(message);
            if (m.find())
            {
                String expr = m.group(1);
                String attrMessage = "Error evaluating attribute expression: "
                    + "<span style=\"color:red;font-weight:bold\"><code>@{"
                    + expr
                    + "}</code></span>\n";
                String details = message.substring(m.end());

                // For token parsing errors
                m = TOKEN_PARSING_MSG.matcher(details);
                if (m.find())
                {
                    details = "Parsing error " + m.group(1).toLowerCase()
                        + details.substring(m.end());
                }

                m = LINE_AND_COLUMN_INFO.matcher(details);
                details = m.replaceFirst("");

                if (details.startsWith(" : Void assignment"))
                {
                    if (expr.indexOf(')') >= 0)
                    {
                        details = "A <code>void</code> method was called "
                            + "where a value was expected.";
                    }
                    else
                    {
                        details = "Unrecognized variable name \"<b>"
                            + expr + "</b>\"";
                    }
                }

                message = attrMessage + details;
            }
            m = VARIABLE_VALUE.matcher(message);
            if (m.find())
            {
                message = message.substring(0, m.start(1))
                    + "<span style=\"color:red;font-weight:bold\"><code>"
                    + m.group(1)
                    + "</code></span>"
                    + message.substring(m.end(1), m.start(2))
                    + "<b><code>"
                    + m.group(2)
                    + "</code></b>"
                    + message.substring(m.end(2));
            }
            m = METHOD_INVOCATION_MSG.matcher(message);
            System.out.println("checking " + message);
            if (m.find())
            {
                System.out.println("found!");
                message = message.substring(0, m.start())
                    + message.substring(m.start() + 2, m.start(1))
                    + "<span style=\"color:red;font-weight:bold\"><code>"
                    + m.group(1)
                    + "</code></span>"
                    + message.substring(m.end(1), m.start(2))
                    + "<b><code>"
                    + m.group(2)
                    + "</code></b>"
                    + message.substring(m.end(2));
            }
//            System.out.println(" after first step:" + message);
//          message = message.replaceAll("((^| : )([^:]+)):", "$2<b>$3</b>:");
//          message = message.replaceAll(" :\\s+", "<br/>\n");
//            message = message.replaceAll("\n\\s*:\\s+", "\n");
//            message = message.replaceAll("[ \t]", "&nbsp;");
//          message = LINE_AND_COLUMN_INFO.matcher(message).replaceAll(
//              "<span style=\"color:red\">$1</span>");
            message = message.replaceAll("\n", "<br/>\n");
            message = message.replaceAll("(\\sclass)(&#039;)", "$1 $2");
//            message = message.replaceAll("<span-style=", "<span style=");
        }
        else if (exception instanceof org.xml.sax.SAXParseException)
        {
            String quote = escapeHtml("\"");
            message = message.replaceAll(
                "([Ee]lement\\s+(type\\s+)?" + quote + "[^\\s]*" + quote + ")",
                "<b>$1</b>");
            message = message.replaceAll(
                "((matching\\s+)?end-tag\\s+" + quote + "[^\\s]*" + quote + ")",
                "<b>$1</b>");
            message = message.replaceAll(
                "\\s*This\\s+error\\s+is\\s+called\\s+a\\s+"
                + "SAXParseException\\.\\s*$",
                "");
        }
        else if (exception instanceof
            org.zkoss.zk.ui.metainfo.PropertyNotFoundException
            && message != null)
        {
            Matcher m = BAD_TAG_ATTRIBUTE.matcher(message);
            if (m.matches())
            {
                String varName = m.group(1);
                String tagName = m.group(2);
                if (varName.length() > 0)
                {
                    varName = varName.substring(0, 1).toLowerCase()
                        + varName.substring(1);
                }
                int pos = tagName.lastIndexOf('.');
                if (pos > 0)
                {
                    tagName = tagName.substring(pos + 1);
                }
                if (tagName.length() > 0)
                {
                    tagName = tagName.substring(0, 1).toLowerCase()
                        + tagName.substring(1);
                }
                message = "Unrecognized attribute "
                    + "<span style=\"color:red;font-weight:bold\"><code>"
                    + varName
                    + "</code></span> on tag <b><code>&lt;"
                    + tagName
                    + "&gt;</code></b>";
            }
        }
//        System.out.println("  reformatted as:" + message);

        return message;
    }


    // ----------------------------------------------------------
    /**
     * Generate a printable stack trace for the full set of stack frames
     * associated with this exception.  The trace looks similar to one
     * generated by {@link Throwable#printStackTrace()}, except that it
     * does not contain the exception name/message at the top and the
     * lines are not prefixed with tab characters.
     * @param t The exception to inspect
     * @return The full stack trace
     */
    public static String fullStackTrace(Throwable t)
    {
        StringWriter sw = new StringWriter();
        for (StackTraceElement frame : t.getStackTrace())
        {
            sw.append("at ");
            sw.append(frame.toString());
            sw.append("\n");
        }
        return sw.toString();
    }


    // ----------------------------------------------------------
    /**
     * Generate a simplified stack trace for this exception.  This is a
     * subset of the full stack trace where appropriate system-oriented
     * packages in upper levels of the stack have been omitted to focus
     * attention on the client code that led to the error.
     * @param t The exception to inspect
     * @return The stack trace.  May return an empty string if no client
     * or course-specific classes can be found anywhere in the exception's
     * stack frames.
     */
    public static String simplifiedStackTrace(Throwable t)
    {
        int topSystemFrame = -1;
        int topUserFrame = -1;
        StackTraceElement[] frames = t.getStackTrace();
        for (int i = frames.length - 1; i > 0; i--)
        {
            String name = frames[i].getClassName();
            if (!classMatchesPrefix(name, CLASS_PREFIXES_TO_SKIP))
            {
                if (topSystemFrame < i)
                {
                    topSystemFrame = i;
                }
                if (!classMatchesPrefix(name, INFRASTRUCTURE_CLASS_PREFIXES))
                {
                    topUserFrame = i;
                    if (i == frames.length - 1)
                    {
                        // Ack! This would flag the entire stack trace, so
                        // there is no "simplified" version
                        topSystemFrame = -1;
                    }
                    break;
                }
            }
        }
//        System.out.println("simplifying stack: top user = " + topUserFrame
//            + ", top system = " + topSystemFrame);
        if (topSystemFrame < 0)
        {
            return null;
        }
        StringWriter sw = new StringWriter();
        for (int i = 0; i <= topSystemFrame; i++)
        {
            if (i <= topUserFrame || !classMatchesPrefix(
                frames[i].getClassName(), CLASS_PREFIXES_TO_SKIP))
            {
                sw.append("at ");
                sw.append(frames[i].toString());
                sw.append("\n");
            }
        }
        return sw.toString();
    }


    // ----------------------------------------------------------
    /**
     * Try to generate a simplified stack trace for this exception, or
     * generate a full trace if no simple trace can be created.
     * @param t The exception to inspect
     * @return The stack trace
     */
    public static String bestStackTrace(Throwable t)
    {
        String result = simplifiedStackTrace(t);
        if (result == null)
        {
            result = fullStackTrace(t);
        }
        return result;
    }


    // ----------------------------------------------------------
    /**
     * Tests a class name to see if it begins with any of a list of
     * prefixes.
     * @param className The name to check
     * @param prefixes  An array of simple substring prefixes to check for
     * @return True if this class matches any prefix
     */
    public static boolean classMatchesPrefix(
        String className, String[] prefixes)
    {
        for (String prefix : prefixes)
        {
            if (className.startsWith(prefix))
            {
                return true;
            }
        }
        return false;
    }


    // ----------------------------------------------------------
    /**
     * Add a grid component with exception details and a stack trace
     * to a ZK page.
     * @param component The component to page the grid on as a child
     * @param t The exception to generate details for
     * @param location An optional string indicating where the exception
     * occurred or what was being attempted, purely for user information.  If
     * null, it will be omitted from the grid.
     */
    public static void addSimpleExceptionGrid(
        Component component,
        Throwable t,
        String    location)
    {
        addSimpleExceptionGrid(component, t, location, true);
    }


    // ----------------------------------------------------------
    /**
     * Add a grid component with exception details to a ZK page.
     * @param component The component to page the grid on as a child
     * @param t The exception to generate details for
     * @param location An optional string indicating where the exception
     * occurred or what was being attempted, purely for user information.  If
     * null, it will be omitted from the grid.
     * @param showStackTrace Whether or not the exception's stack trace should
     * be shown in the grid.
     */
    public static void addSimpleExceptionGrid(
        Component component,
        Throwable t,
        String    location,
        boolean   showStackTrace)
    {
        t = getRealCause(t);
        Window window = new Window("Unexpected Exception", "normal", false);
        window.setContentStyle("padding:0;margin:0");
        component.appendChild(window);
        Component grid = new Grid();
        window.appendChild(grid);
        Component rows = new Rows();
        grid.appendChild(rows);
        Component row = null;

        if (location != null)
        {
            row = new Row();
            rows.appendChild(row);
            row.appendChild(new Text("Location:"));
            row.appendChild(new Text(location));
        }
        row = new Row();
        rows.appendChild(row);
        row.appendChild(new Text("Exception:"));
        row.appendChild(new Text(simplifiedExceptionName(t)));

        String message = highlightExceptionMessage(t);
        if (message != null)
        {
            row = new Row();
            rows.appendChild(row);
            row.appendChild(new Text("Message:"));
            row.appendChild(new Text(message));
        }

        if (showStackTrace)
        {
            String trace = escapeHtml(bestStackTrace(t));
            if (trace != null)
            {
                row = new Row();
                rows.appendChild(row);
                row.appendChild(new Text("Stack trace:"));
                Component pre = new Pre();
                row.appendChild(pre);
                pre.appendChild(new Text(trace));
            }
        }
    }


    // ----------------------------------------------------------
    /**
     * Escape the HTML special characters in a string.  This means replacing
     * the following:
     * <ul>
     * <li>&amp; by &amp;amp;</li>
     * <li>&lt; by &amp;lt;</li>
     * <li>&gt; by &amp;gt;</li>
     * <li>&#034; by &amp;#034;</li>
     * <li>&#039; by &amp;#039;</li>
     * </ul>
     * @param str The string to translate
     * @return The corresponding string with HTML special characters
     * substituted by their escape sequences.
     */
    public static String escapeHtml(String str)
    {
        if (str != null)
        {
            str = str
                .replaceAll("&",  "&amp;")
                .replaceAll("<",  "&lt;")
                .replaceAll(">",  "&gt;")
                .replaceAll("\"", "&#034;")
                .replaceAll("'",  "&#039;");
        }
        return str;
    }


    //~ Instance/static variables .............................................

    private static final String CLASS_PREFIXES_TO_SKIP[] = {
        "java.",
        "javax.",
        "org.zkoss.",
        "sun.",
        "com.sun.",
        "org.apache.",
        "bsh.",
        "org.springframework.",
        "student.web.internal.",
        "student.web.",
        "cloudspace."
    };

    private static final String INFRASTRUCTURE_CLASS_PREFIXES[] = {
        "student.",
        "cs1705."
    };

    private static final Pattern BAD_TAG_ATTRIBUTE =
        Pattern.compile("Method set([\\p{javaUpperCase}\\p{javaLowerCase}"
            + "\\p{Digit}_]+) not found for class\\s+(.*\\S)\\s*");
    private static final Pattern BAD_AT_EXPRESSION =
        Pattern.compile("Sourced\\s+file:\\s+inline\\s+.*_value\\.bean(?:\\s+"
            + "=\\s*.*|\\));/\\*(.*)\\*/;\\s*\n", Pattern.DOTALL);
    private static final Pattern VARIABLE_VALUE =
        Pattern.compile("Unable to evaluate value\\s+(.*)\\s+"
            + "for (?:<|&lt;)variable(?:>|&gt;) tag with name "
            + "(?:\"|&#034;)(.+)(?:\"|&#034;)");
    private static final Pattern METHOD_INVOCATION_MSG =
        Pattern.compile(": Error in method invocation: Method "
            + "(.*) not found in class\\s*(?:'|&#039;)(.*)(?:'|&#039;)");
    private static final Pattern TOKEN_PARSING_MSG =
        Pattern.compile("Token Parsing Error:[^\\.]*\\.\\s*(\\S)");
    private static final Pattern TOKEN_PARSING_EOF_MSG =
        Pattern.compile("Token Parsing Error: .* <EOF>.*");
    private static final Pattern PARSE_ERROR_MSG =
        Pattern.compile("\\s*:\\s+Parse error.*\\.\\s+Encountered.*");
    private static final Pattern CANNOT_ACCESS_FIELD_MSG =
        Pattern.compile("Cannot access field: (\\S*),");
    private static final Pattern LINE_AND_COLUMN_INFO =
        Pattern.compile("\\s*at line 1, column [1-9][0-9]*");
}